// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CONTENT_BROWSER_SERVICE_WORKER_SERVICE_WORKER_DATABASE_H_
#define CONTENT_BROWSER_SERVICE_WORKER_SERVICE_WORKER_DATABASE_H_

#include <stdint.h>

#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/files/file_path.h"
#include "base/gtest_prod_util.h"
#include "base/macros.h"
#include "base/optional.h"
#include "base/sequence_checker.h"
#include "base/time/time.h"
#include "content/common/content_export.h"
#include "content/common/origin_trials/trial_token_validator.h"
#include "content/common/service_worker/service_worker_status_code.h"
#include "content/common/service_worker/service_worker_types.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace leveldb {
class DB;
class Env;
class Status;
class WriteBatch;
}

namespace content {

// Class to persist serviceworker registration data in a database.
// Should NOT be used on the IO thread since this does blocking
// file io. The ServiceWorkerStorage class owns this class and
// is responsible for only calling it serially on background
// non-IO threads (ala SequencedWorkerPool).
class CONTENT_EXPORT ServiceWorkerDatabase {
public:
    // We do leveldb stuff in |path| or in memory if |path| is empty.
    explicit ServiceWorkerDatabase(const base::FilePath& path);
    ~ServiceWorkerDatabase();

    // Used in UMA. A new value must be appended only.
    enum Status {
        STATUS_OK,
        STATUS_ERROR_NOT_FOUND,
        STATUS_ERROR_IO_ERROR,
        STATUS_ERROR_CORRUPTED,
        STATUS_ERROR_FAILED,
        STATUS_ERROR_NOT_SUPPORTED,
        STATUS_ERROR_MAX,
    };
    static const char* StatusToString(Status status);

    struct CONTENT_EXPORT RegistrationData {
        // These values are immutable for the life of a registration.
        int64_t registration_id;
        GURL scope;

        // Versions are first stored once they successfully install and become
        // the waiting version. Then transition to the active version. The stored
        // version may be in the ACTIVATED state or in the INSTALLED state.
        GURL script;
        int64_t version_id;
        bool is_active;
        bool has_fetch_handler;
        base::Time last_update_check;
        std::vector<GURL> foreign_fetch_scopes;
        std::vector<url::Origin> foreign_fetch_origins;
        base::Optional<TrialTokenValidator::FeatureToTokensMap> origin_trial_tokens;
        NavigationPreloadState navigation_preload_state;

        // Not populated until ServiceWorkerStorage::StoreRegistration is called.
        int64_t resources_total_size_bytes;

        RegistrationData();
        RegistrationData(const RegistrationData& other);
        ~RegistrationData();
    };

    struct ResourceRecord {
        int64_t resource_id;
        GURL url;
        // Signed so we can store -1 to specify an unknown or error state.  When
        // stored to the database, this value should always be >= 0.
        int64_t size_bytes;

        ResourceRecord()
            : resource_id(-1)
            , size_bytes(0)
        {
        }
        ResourceRecord(int64_t id, GURL url, int64_t size_bytes)
            : resource_id(id)
            , url(url)
            , size_bytes(size_bytes)
        {
        }
    };

    // Reads next available ids from the database. Returns OK if they are
    // successfully read. Fills the arguments with an initial value and returns
    // OK if they are not found in the database. Otherwise, returns an error.
    Status GetNextAvailableIds(int64_t* next_avail_registration_id,
        int64_t* next_avail_version_id,
        int64_t* next_avail_resource_id);

    // Reads origins that have one or more than one registration from the
    // database. Returns OK if they are successfully read or not found.
    // Otherwise, returns an error.
    Status GetOriginsWithRegistrations(std::set<GURL>* origins);

    // Reads origins that have one or more than one registration with at least one
    // foreign fetch scope registered. Returns OK if they are successfully read or
    // not found. Otherwise returns an error.
    Status GetOriginsWithForeignFetchRegistrations(std::set<GURL>* origins);

    // Reads registrations for |origin| from the database. Returns OK if they are
    // successfully read or not found. Otherwise, returns an error.
    Status GetRegistrationsForOrigin(
        const GURL& origin,
        std::vector<RegistrationData>* registrations,
        std::vector<std::vector<ResourceRecord>>* opt_resources_list);

    // Reads all registrations from the database. Returns OK if successfully read
    // or not found. Otherwise, returns an error.
    Status GetAllRegistrations(std::vector<RegistrationData>* registrations);

    // Saving, retrieving, and updating registration data.
    // (will bump next_avail_xxxx_ids as needed)
    // (resource ids will be added/removed from the uncommitted/purgeable
    // lists as needed)

    // Reads a registration for |registration_id| and resource records associated
    // with it from the database. Returns OK if they are successfully read.
    // Otherwise, returns an error.
    Status ReadRegistration(int64_t registration_id,
        const GURL& origin,
        RegistrationData* registration,
        std::vector<ResourceRecord>* resources);

    // Looks up the origin for the registration with |registration_id|. Returns OK
    // if a registration was found and read successfully. Otherwise, returns an
    // error.
    Status ReadRegistrationOrigin(int64_t registration_id, GURL* origin);

    // Writes |registration| and |resources| into the database and does following
    // things:
    //   - If an old version of the registration exists, deletes it and sets
    //   |deleted_version| to the old version registration data object
    //   |newly_purgeable_resources| to its resources. Otherwise, sets
    //   |deleted_version->version_id| to -1.
    //   - Bumps the next registration id and the next version id if needed.
    //   - Removes |resources| from the uncommitted list if exist.
    // Returns OK they are successfully written. Otherwise, returns an error.
    Status WriteRegistration(const RegistrationData& registration,
        const std::vector<ResourceRecord>& resources,
        RegistrationData* deleted_version,
        std::vector<int64_t>* newly_purgeable_resources);

    // Updates a registration for |registration_id| to an active state. Returns OK
    // if it's successfully updated. Otherwise, returns an error.
    Status UpdateVersionToActive(int64_t registration_id, const GURL& origin);

    // Updates last check time of a registration for |registration_id| by |time|.
    // Returns OK if it's successfully updated. Otherwise, returns an error.
    Status UpdateLastCheckTime(int64_t registration_id,
        const GURL& origin,
        const base::Time& time);

    // Updates the navigation preload state for the specified registration.
    // Returns OK if it's successfully updated. Otherwise, returns an error.
    Status UpdateNavigationPreloadEnabled(int64_t registration_id,
        const GURL& origin,
        bool enable);
    Status UpdateNavigationPreloadHeader(int64_t registration_id,
        const GURL& origin,
        const std::string& value);

    // Deletes a registration for |registration_id| and moves resource records
    // associated with it into the purgeable list. If deletion occurred, sets
    // |version_id| to the id of the version that was deleted and
    // |newly_purgeable_resources| to its resources; otherwise, sets |version_id|
    // to -1. Returns OK if it's successfully deleted or not found in the
    // database. Otherwise, returns an error.
    Status DeleteRegistration(int64_t registration_id,
        const GURL& origin,
        RegistrationData* deleted_version,
        std::vector<int64_t>* newly_purgeable_resources);

    // Reads user data for |registration_id| and |user_data_names| from the
    // database. Returns OK only if all keys are found; otherwise NOT_FOUND, and
    // |user_data_values| will be empty.
    Status ReadUserData(int64_t registration_id,
        const std::vector<std::string>& user_data_names,
        std::vector<std::string>* user_data_values);

    // Writes |name_value_pairs| into the database. Returns NOT_FOUND if the
    // registration specified by |registration_id| does not exist in the database.
    Status WriteUserData(
        int64_t registration_id,
        const GURL& origin,
        const std::vector<std::pair<std::string, std::string>>& name_value_pairs);

    // Deletes user data for |registration_id| and |user_data_names| from the
    // database. Returns OK if all are successfully deleted or not found in the
    // database.
    Status DeleteUserData(int64_t registration_id,
        const std::vector<std::string>& user_data_names);

    // Reads user data for all registrations that have data with |user_data_name|
    // from the database. Returns OK if they are successfully read or not found.
    Status ReadUserDataForAllRegistrations(
        const std::string& user_data_name,
        std::vector<std::pair<int64_t, std::string>>* user_data);

    // Resources should belong to one of following resource lists: uncommitted,
    // committed and purgeable.
    // As new resources are put into the diskcache, they go into the uncommitted
    // list. When a registration is saved that refers to those ids, they're moved
    // to the committed list. When a resource no longer has any registrations or
    // caches referring to it, it's added to the purgeable list. Periodically,
    // the purgeable list can be purged from the diskcache. At system startup, all
    // uncommitted ids are moved to the purgeable list.

    // Reads resource ids from the uncommitted list. Returns OK on success.
    // Otherwise clears |ids| and returns an error.
    Status GetUncommittedResourceIds(std::set<int64_t>* ids);

    // Writes resource ids into the uncommitted list. Returns OK on success.
    // Otherwise writes nothing and returns an error.
    Status WriteUncommittedResourceIds(const std::set<int64_t>& ids);

    // Reads resource ids from the purgeable list. Returns OK on success.
    // Otherwise clears |ids| and returns an error.
    Status GetPurgeableResourceIds(std::set<int64_t>* ids);

    // Deletes resource ids from the purgeable list. Returns OK on success.
    // Otherwise deletes nothing and returns an error.
    Status ClearPurgeableResourceIds(const std::set<int64_t>& ids);

    // Writes resource ids into the purgeable list and removes them from the
    // uncommitted list. Returns OK on success. Otherwise writes nothing and
    // returns an error.
    Status PurgeUncommittedResourceIds(const std::set<int64_t>& ids);

    // Deletes all data for |origins|, namely, unique origin, registrations and
    // resource records. Resources are moved to the purgeable list. Returns OK if
    // they are successfully deleted or not found in the database. Otherwise,
    // returns an error.
    Status DeleteAllDataForOrigins(
        const std::set<GURL>& origins,
        std::vector<int64_t>* newly_purgeable_resources);

    // Completely deletes the contents of the database.
    // Be careful using this function.
    Status DestroyDatabase();

private:
    // Opens the database at the |path_|. This is lazily called when the first
    // database API is called. Returns OK if the database is successfully opened.
    // Returns NOT_FOUND if the database does not exist and |create_if_missing| is
    // false. Otherwise, returns an error.
    Status LazyOpen(bool create_if_missing);

    // Helper for LazyOpen(). |status| must be the return value from LazyOpen()
    // and this must be called just after LazyOpen() is called. Returns true if
    // the database is new or nonexistent, that is, it has never been used.
    bool IsNewOrNonexistentDatabase(Status status);

    // Reads the next available id for |id_key|. Returns OK if it's successfully
    // read. Fills |next_avail_id| with an initial value and returns OK if it's
    // not found in the database. Otherwise, returns an error.
    Status ReadNextAvailableId(const char* id_key, int64_t* next_avail_id);

    // Reads registration data for |registration_id| from the database. Returns OK
    // if successfully reads. Otherwise, returns an error.
    Status ReadRegistrationData(int64_t registration_id,
        const GURL& origin,
        RegistrationData* registration);

    // Parses |serialized| as a RegistrationData object and pushes it into |out|.
    ServiceWorkerDatabase::Status ParseRegistrationData(
        const std::string& serialized,
        RegistrationData* out);

    // Populates |batch| with operations to write |registration|. It does not
    // actually write to db yet.
    void WriteRegistrationDataInBatch(const RegistrationData& registration,
        leveldb::WriteBatch* batch);

    // Reads resource records for |registration| from the database. Returns OK if
    // it's successfully read or not found in the database. Otherwise, returns an
    // error.
    Status ReadResourceRecords(const RegistrationData& registration,
        std::vector<ResourceRecord>* resources);

    // Parses |serialized| as a ResourceRecord object and pushes it into |out|.
    ServiceWorkerDatabase::Status ParseResourceRecord(
        const std::string& serialized,
        ResourceRecord* out);

    void WriteResourceRecordInBatch(const ResourceRecord& resource,
        int64_t version_id,
        leveldb::WriteBatch* batch);

    // Deletes resource records for |version_id| from the database. Returns OK if
    // they are successfully deleted or not found in the database. Otherwise,
    // returns an error.
    Status DeleteResourceRecords(int64_t version_id,
        std::vector<int64_t>* newly_purgeable_resources,
        leveldb::WriteBatch* batch);

    // Reads resource ids for |id_key_prefix| from the database. Returns OK if
    // it's successfully read or not found in the database. Otherwise, returns an
    // error.
    Status ReadResourceIds(const char* id_key_prefix, std::set<int64_t>* ids);

    // Write resource ids for |id_key_prefix| into the database. Returns OK on
    // success. Otherwise, returns writes nothing and returns an error.
    Status WriteResourceIdsInBatch(const char* id_key_prefix,
        const std::set<int64_t>& ids,
        leveldb::WriteBatch* batch);

    // Deletes resource ids for |id_key_prefix| from the database. Returns OK if
    // it's successfully deleted or not found in the database. Otherwise, returns
    // an error.
    Status DeleteResourceIdsInBatch(const char* id_key_prefix,
        const std::set<int64_t>& ids,
        leveldb::WriteBatch* batch);

    // Deletes all user data for |registration_id| from the database. Returns OK
    // if they are successfully deleted or not found in the database.
    Status DeleteUserDataForRegistration(int64_t registration_id,
        leveldb::WriteBatch* batch);

    // Reads the current schema version from the database. If the database hasn't
    // been written anything yet, sets |db_version| to 0 and returns OK.
    Status ReadDatabaseVersion(int64_t* db_version);

    // Writes a batch into the database.
    // NOTE: You must call this when you want to put something into the database
    // because this initializes the database if needed.
    Status WriteBatch(leveldb::WriteBatch* batch);

    // Bumps the next available id if |used_id| is greater than or equal to the
    // cached one.
    void BumpNextRegistrationIdIfNeeded(int64_t used_id,
        leveldb::WriteBatch* batch);
    void BumpNextResourceIdIfNeeded(int64_t used_id, leveldb::WriteBatch* batch);
    void BumpNextVersionIdIfNeeded(int64_t used_id, leveldb::WriteBatch* batch);

    bool IsOpen();

    void Disable(
        const tracked_objects::Location& from_here,
        Status status);
    void HandleOpenResult(
        const tracked_objects::Location& from_here,
        Status status);
    void HandleReadResult(
        const tracked_objects::Location& from_here,
        Status status);
    void HandleWriteResult(
        const tracked_objects::Location& from_here,
        Status status);

    const base::FilePath path_;
    std::unique_ptr<leveldb::Env> env_;
    std::unique_ptr<leveldb::DB> db_;

    int64_t next_avail_registration_id_;
    int64_t next_avail_resource_id_;
    int64_t next_avail_version_id_;

    enum State {
        UNINITIALIZED,
        INITIALIZED,
        DISABLED,
    };
    State state_;

    bool IsDatabaseInMemory() const;

    base::SequenceChecker sequence_checker_;

    FRIEND_TEST_ALL_PREFIXES(ServiceWorkerDatabaseTest, OpenDatabase);
    FRIEND_TEST_ALL_PREFIXES(ServiceWorkerDatabaseTest, OpenDatabase_InMemory);
    FRIEND_TEST_ALL_PREFIXES(ServiceWorkerDatabaseTest,
        DatabaseVersion_ValidSchemaVersion);
    FRIEND_TEST_ALL_PREFIXES(ServiceWorkerDatabaseTest,
        DatabaseVersion_ObsoleteSchemaVersion);
    FRIEND_TEST_ALL_PREFIXES(ServiceWorkerDatabaseTest,
        DatabaseVersion_CorruptedSchemaVersion);
    FRIEND_TEST_ALL_PREFIXES(ServiceWorkerDatabaseTest, GetNextAvailableIds);
    FRIEND_TEST_ALL_PREFIXES(ServiceWorkerDatabaseTest,
        Registration_UninitializedDatabase);
    FRIEND_TEST_ALL_PREFIXES(ServiceWorkerDatabaseTest,
        UserData_UninitializedDatabase);
    FRIEND_TEST_ALL_PREFIXES(ServiceWorkerDatabaseTest, DestroyDatabase);

    DISALLOW_COPY_AND_ASSIGN(ServiceWorkerDatabase);
};

} // namespace content

#endif // CONTENT_BROWSER_SERVICE_WORKER_SERVICE_WORKER_DATABASE_H_
